#include "Mesh.h"
#include <list>
#include <sstream>
#include <stdexcept>
#include <glad/glad.h>
#include <assimp/Importer.hpp>
#include <assimp/postprocess.h>
#include <assimp/scene.h>
#include "util.h"

using namespace std;
using namespace glm;
using namespace Assimp;

auto offsetM1 = glm::mat4(
    1.0f, 0.0f, 0.0f, 0.0f,
    0.0f, 0.0f, 1.0f, 0.0f,
    0.0f, -1.0f, 0.0f, 0.0f,
    0.0f, 0.0f, 0.0f, 1.0f
    );

auto offsetM2 = glm::mat4(
        1.0f, 0.0f, 0.0f, 0.0f,
        0.0f, 0.0f, -1.0f, 0.0f,
        0.0f, 1.0f, 0.0f, 0.0f,
        0.0f, 0.0f, 0.0f, 1.0f
);

Mesh::Mesh(const vector<Vertex> &vertices, const vector<uint32_t> &indices)
{
    this -> vertices = vertices;
    this -> indices = indices;

    VAO = 0;
    VBO = 0;
    EBO = 0;

    modelMatrix = glm::mat4(1.0f);

    SetupMesh();
}

void Mesh::SetupMesh()
{
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);
    glGenBuffers(1, &EBO);

    glBindVertexArray(VAO);

    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, int64_t(vertices.size() * sizeof(Vertex)), vertices.data(), GL_STATIC_DRAW);

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, int64_t(indices.size() * sizeof(uint32_t)), indices.data(), GL_STATIC_DRAW);

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Position));
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Normal));

    glEnableVertexAttribArray(0);
    glEnableVertexAttribArray(1);

    glBindVertexArray(0);
}

void Mesh::Draw(const gl::IShader &shader) const
{
    glActiveTexture(GL_TEXTURE0);

    auto m = worldMatrix * offsetM2 * modelMatrix * offsetMatrix * offsetM1;

    shader.setMat3("normalMatrix", glm::transpose(glm::inverse(glm::mat3(m))));
    shader.setMat4("modelMatrix", m);

    glBindVertexArray(VAO);
    glDrawElements(GL_TRIANGLES, int32_t(indices.size()), GL_UNSIGNED_INT, nullptr);
    glBindVertexArray(0);
    glActiveTexture(GL_TEXTURE0);
}

void Mesh::SetWorldMatrix(const mat4 &mMat)
{
    worldMatrix = mMat;
}

void Mesh::SetOffsetMatrix(const mat4 &mMat)
{
    offsetMatrix = mMat;
}

void Mesh::SetModelMatrix(const mat4 &mMat)
{
    modelMatrix = mMat;
}

std::shared_ptr<Mesh> Mesh::LoadMeshFromFile(const string &path)
{
    Importer importer;
    const aiScene *scene = importer.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs | aiProcess_GenNormals);

    if(!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode)
    {
        stringstream ss;
        ss << "Failed to load file(" << path << "): ERROR::ASSIMP::" << importer.GetErrorString();
        throw invalid_argument(ss.str());
    }

    if(scene->mNumMeshes != 1)
    {
        stringstream ss;
        if(scene->mNumMeshes > 1)
            ss << "Failed to load file(" << path << "): More than one mesh in this file.";
        if(scene->mNumMeshes == 0)
            ss << "Failed to load file(" << path << "): No mesh in this file.";
        throw invalid_argument(ss.str());
    }

    auto& mesh = scene->mMeshes[0];
    list<Vertex> vertices_l;
    list<uint32_t> indices_l;

    for (int j = 0; j < mesh->mNumVertices; ++j)
    {
        Vertex v{};

        v.Position.x = mesh->mVertices[j].x;
        v.Position.y = mesh->mVertices[j].y;
        v.Position.z = mesh->mVertices[j].z;

        if (mesh->mNormals != nullptr)
        {
            v.Normal.x = mesh->mNormals[j].x;
            v.Normal.y = mesh->mNormals[j].y;
            v.Normal.z = mesh->mNormals[j].z;
        }

        vertices_l.emplace_back(v);
    }

    for (int j = 0; j < mesh->mNumFaces; ++j)
    {
        const aiFace& face = mesh->mFaces[j];
        for (int k = 0; k < face.mNumIndices; ++k)
            indices_l.emplace_back(face.mIndices[k]);
    }

    vector<Vertex> vertices;
    vector<uint32_t> indices;

    util::list2vector(vertices_l, vertices);
    util::list2vector(indices_l, indices);

    return make_shared<Mesh>(vertices, indices);
}

